/*
 * The MIT License
 *
 * Copyright 2014 David Rodenas Pico.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package edu.uoc.sd.fr14;

import edu.uoc.sd.fr14.exp.Experiment;
import edu.uoc.sd.fr14.model.AbstractServer;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.Properties;
import java.util.Set;

import edu.uoc.sd.fr14.model.CustomerId;
import edu.uoc.sd.fr14.model.ServerId;

import java.net.InetSocketAddress;
import java.util.LinkedHashSet;

/**
 * This is a configuration.
 * 
 * 
 * @author drodenas
 */
public class Configuration {
    
    /* ******************************************************
     * *
     * * CONFIGURATION DEFAULTS
     * *
     * ******************************************************/
    
    private static Properties createDefaults() {
        
        Properties defaults = new Properties();
        defaults.setProperty("config", "fr14.conf");
        // experiments
        defaults.setProperty("experimentClass", "edu.uoc.sd.fr14.exp.ExperimentPhase3");
        defaults.setProperty("executionTime",    "30");
        defaults.setProperty("customerDelay",     "2");
        defaults.setProperty("shutdownDelay",     "2");
        defaults.setProperty("experimentTick", "1000");
        defaults.setProperty("disconnectProbability","0.1");
        defaults.setProperty("reconnectProbability","0.5");
        // customers
        defaults.setProperty("customers",      "10");
        defaults.setProperty("customer", "localhost:43210");
        defaults.setProperty("operator",      "yes");
        defaults.setProperty("customerSeat",    "0.05");
        defaults.setProperty("customerConfirm", "0.1");
        defaults.setProperty("customerQuery",   "0.2");
        // servers
        defaults.setProperty("servers", "2");
        defaults.setProperty("server1", "localhost:43211");
        defaults.setProperty("server2", "localhost:43212");
        defaults.setProperty("server3", "localhost:43213");
        defaults.setProperty("me", "1,2");
        defaults.setProperty("serverClass", "edu.uoc.sd.fr14.impl.StudentServer");        
        defaults.setProperty("timeoutElectionBegin", "1000");
        defaults.setProperty("timeoutElectionEnd",   "1000");
        defaults.setProperty("timeoutSynchronize",  "10000");
        defaults.setProperty("timeoutCustomer",      "1000");
        // debug
        defaults.setProperty("debugExperiment",         "yes");
        defaults.setProperty("debugExperimentComponent","no");
        defaults.setProperty("debugToggleComponent",    "no");
        defaults.setProperty("debugRemoteComponent",    "no");
        defaults.setProperty("debugListenerComponent",  "no");
        defaults.setProperty("debugCustomerComponent",  "no");
        defaults.setProperty("debugClockComponent",     "no");
        defaults.setProperty("debugServerComponent",    "yes");
        defaults.setProperty("debugModelFlight",        "no");
        defaults.setProperty("debugModelNseqTracker",   "no");
        defaults.setProperty("debugModelLog",           "no");
        defaults.setProperty("debugModelOp",            "no");
        defaults.setProperty("debugModelRole",          "no");
        
        return defaults;
    }
        
    /* ******************************************************
     * *
     * * BUILD CONFIGURATION
     * *
     * ******************************************************/

    private Properties config;
    
    public Configuration() {
        this(new String[0]);
    }

    public Configuration(Properties properties) {
        this(new String[0], properties);
    }

    public Configuration(String[] args) {
        this(args, new Properties());
    }
    
    public Configuration(Configuration config) {
        this(config.config);
    }
    
    public Configuration(String[] args, Properties properties) {
        if (args == null) throw new NullPointerException("Illegal argument: args cannot be null");
        
        Properties defaults = createDefaults();
        Properties options = parseOptions(defaults, args);
        config = readConfig(defaults, options);
        
        config.putAll(properties);
    }
    
    public static Configuration create(Configuration config, ServerId serverId, boolean operator) {
        Configuration result = new Configuration();
        if (serverId != null)
            result.config.setProperty("me", Integer.toString(serverId.getNumber()));
        else
            result.config.setProperty("me", "");
        result.config.setProperty("operator", operator?"yes":"no");
        return result;
    }
        
    public void addConfiguration(Properties config) {
        this.config.putAll(config);
    }
    
    

    /* ******************************************************
     * *
     * * CONFIGURATION HIGH LEVEL GETTERS
     * *
     * ******************************************************/
    
    /* ***** experiment ******************************************************/
    
    public Class<Experiment> experimentClass() {
        String experimentClass = config.getProperty("experimentClass");
        try {
            @SuppressWarnings("unchecked")
			Class<Experiment> clazz = (Class<Experiment>) Class.forName(experimentClass);
            return clazz;
        } catch (ClassNotFoundException ex) {
            throw new IllegalStateException("Illegal configuration: experimentClass '"+experimentClass+"' cannot be instantiated", ex);
        }
    }
    
    public ExperimentConfiguration experiment() {
        
        int executionTime = numberFromConfig("executionTime");
        int customerDelay = numberFromConfig("customerDelay");
        int shutdownDelay = numberFromConfig("shutdownDelay");
        long experimentTick = numberFromConfig("experimentTick");
        double disconnectProbability = floatFromConfig("disconnectProbability");
        double reconnectProbability = floatFromConfig("reconnectProbability");
        
        return new ExperimentConfiguration(executionTime, customerDelay, shutdownDelay, experimentTick, disconnectProbability, reconnectProbability);
    }
    
    /* ***** customers *******************************************************/
    
    public Set<CustomerId> customers() {
        
        Set<CustomerId> customers = new LinkedHashSet<>();
        int customerCount = numberFromConfig("customers");
        for (int number = 0; number < customerCount; number++) {
            CustomerId customerId = new CustomerId(1+number);
            customers.add(customerId);
        }
        
        if (customers.isEmpty()) {
            throw new IllegalStateException("Illegal configuration: there should be at least one consumer, although it is not used.");
        }
        
        return customers;
    }
    
    public InetSocketAddress customer() {

        InetSocketAddress address = addressFromConfig("customer");
        return address;
    }
    
    public boolean operator() {

        boolean operator = booleanFromConfig("operator");
        return operator;
    }
    
    /* ***** servers *********************************************************/
    
    public ClockConfiguration clockConfiguration() {
        
        int electionBeginTo = numberFromConfig("timeoutElectionBegin");
        int electionEndTo = numberFromConfig("timeoutElectionEnd");
        int synchronizeTo = numberFromConfig("timeoutSynchronize");
        
        return new ClockConfiguration(electionBeginTo, electionEndTo, synchronizeTo);
    }
    
    public CustomerConfiguration customerConfiguration() {
        
        int timeoutCustomer = numberFromConfig("timeoutCustomer");
        double customerSeat = floatFromConfig("customerSeat");
        double customerConfirm = floatFromConfig("customerConfirm");
        double customerQuery = floatFromConfig("customerQuery");
        
        return new CustomerConfiguration(timeoutCustomer, customerSeat, customerConfirm, customerQuery);
    }

    public Set<ServerId> servers() {
        Set<ServerId> servers = new LinkedHashSet<>();
        int serverCount = numberFromConfig("servers");
        for (int number = 1; number <= serverCount; number++) {
            ServerId serverId = new ServerId(number);
            servers.add(serverId);
        }
        return servers;
    }
    
    InetSocketAddress server(ServerId serverId) {
        
        InetSocketAddress address = addressFromConfig("server"+serverId.getNumber());        
        return address;
    }
        
    public Class<AbstractServer> serverClass() {
        String serverClass = config.getProperty("serverClass");
        try {
        	@SuppressWarnings("unchecked")
			Class<AbstractServer> clazz = (Class<AbstractServer>)Class.forName(serverClass);
            return clazz;
        } catch (ClassNotFoundException ex) {
            throw new IllegalStateException("Illegal configuration: serverClass '"+serverClass+"' cannot be instantiated", ex);
        }
    }
    
    public Set<ServerId> me() {
        Set<ServerId> localServers = new LinkedHashSet<>();

        try {
            String mes = config.getProperty("me");
            if (mes.length() > 0) {
                String[] values = mes.split(",");
                for (String value : values) {
                    int number = Integer.parseInt(value);
                    ServerId serverId = new ServerId(number);
                    localServers.add(serverId);
                }
            }
        } catch (NumberFormatException nfe) {
            throw new IllegalStateException("Illegal configuration: me should be a comma separated list of numbers", nfe);
        }
        
        return localServers;
    }
    
    /* ***** debug ***********************************************************/
    
    public boolean debug(String part) {
        
        boolean debug = booleanFromConfig("debug"+part);
        return debug;
    }
    

    /* ******************************************************
     * *
     * * READ CONFIGURATION FROM ARGUMENTS
     * *
     * ******************************************************/
    
    private Properties parseOptions(Properties defaults, String[] args) {
        
        Properties options = new Properties();
        String value, v1,v2;
        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--servers":
                    value = optionInt(args, ++i, "--servers", "COUNT");
                    options.setProperty("servers", value);
                    break;
                case "--server":
                    v1 = optionInt(args, ++i, "--server", "ID");
                    v2 = optionString(args, ++i, "--server", "ADDRESS");
                    options.setProperty("server"+v1, v2);
                    break;
                case "--me":
                    v1 = optionString(args, ++i, "--me", "ID/IDS");
                    if (v1.equals("-")) { v1 = ""; }
                    options.setProperty("me",v1);
                    break;
                case "--server-class":
                    value = optionString(args, ++i, "--server-instance", "CLASSNAME");
                    options.setProperty("serverClass", value);
                    break;
                case "--customers":
                    value = optionInt(args, ++i, "--customers", "COUNT");
                    options.setProperty("customers", value);
                    break;
                case "--customer":
                    v2 = optionString(args, ++i, "--customer", "ADDRESS");
                    options.setProperty("customer", v2);
                    break;
                case "--operator":
                    v2 = optionString(args, ++i, "--operator", "OP");
                    options.setProperty("operator", v2);
                    break;
                case "--timeout-election-begin":
                    value = optionInt(args, ++i, "--timeout-election-begin", "MS");
                    options.setProperty("timeoutElectionBegin", value);
                    break;
                case "--timeout-election-end":
                    value = optionInt(args, ++i, "--timeout-election-begin", "MS");
                    options.setProperty("timeoutElectionEnd", value);
                    break;
                case "--timeout-synchronize":
                    value = optionInt(args, ++i, "--timeout-synchronize", "MS");
                    options.setProperty("timeoutSynchronize", value);
                    break;
                case "--timeout-customer":
                    value = optionInt(args, ++i, "--timeout-customer", "MS");
                    options.setProperty("timeoutCustomer", value);
                    break;
                case "--customer-seat":
                    value = optionFloat(args, ++i, "--customer-seat", "PROB");
                    options.setProperty("customerSeat", value);
                    break;
                case "--customer-confirm":
                    value = optionFloat(args, ++i, "--customer-confirm", "PROB");
                    options.setProperty("customerConfirm", value);
                    break;
                case "--customer-query":
                    value = optionFloat(args, ++i, "--customer-query", "PROB");
                    options.setProperty("customerQuery", value);
                    break;
                case "-c":
                case "--config":
                    value = optionString(args, ++i, "--config", "FILENAME");
                    options.setProperty("config", value);
                    break;
                case "-p":
                case "--phase":
                    value = optionString(args, ++i, "--phase", "PHASE");
                    options.setProperty("experimentClass", "edu.uoc.sd.fr14.exp.Experiment"+value);
                    break;
                case "-h":
                case "--help":
                    printUsange(System.out);
                    System.exit(0);
                default:
                    optionError("Error: unknown option '"+args[i]+"'");
            }
        }
        
        return options;
    }
    
    private static String optionString(String[] args, int i, String option, String name) {
        if (args.length <= i) {
            optionError("Error: configuration option '"+option+"' requires '"+name+"'");
        }
        return args[i];
    }
    
    private static String optionInt(String[] args, int i, String option, String name) {
        try {
            int result = Integer.parseInt(optionString(args, i, option, name));
            return String.valueOf(result);
        } catch (NumberFormatException e) {
            optionError("Error: configuration option '"+option+"' requires '"+name+"' to be an integer");
            return "";
        }
    }
    
    private static String optionFloat(String[] args, int i, String option, String name) {
        try {
            double result = Double.parseDouble(optionString(args, i, option, name));
            return String.valueOf(result);
        } catch (NumberFormatException e) {
            optionError("Error: configuration option '"+option+"' requires '"+name+"' to be an float");
            return "";
        }
    }
    
    private static void optionError(String message) {
        System.err.println(message);
        printUsange(System.err);
        System.exit(1);
    }
    
    private static void printUsange(PrintStream out) {
        out.println("Usange:");
        out.println("  --servers COUNT             number of server instances in the cluster");
        out.println("  --server ID ADDRESS         sets the addres for a given server");
        out.println("  --me ID                     ID of the instance server or a comma sepparated list of ids if all in the same machine, - for noone");
        out.println("  --server-class CLASSNAME    full class name of the default server instance");
        out.println("  --customers COUNT           number of client instances");
        out.println("  --customer ADDRESS          address where to find all customer");
        out.println("  --operator OP               yes/no if customers are here");
        out.println("  --timeout-election-begin MS milliseconds for timeout election begin");
        out.println("  --timeout-election-end MS   milliseconds for timeout election end");
        out.println("  --timeout-synchronize MS    period in milliseconds for timeout synchronization");
        out.println("  --timeout-customer MS       period in milliseconds for timeout customer activity");
        out.println("  --customer-seat PROB        probability to generate a seat operation");
        out.println("  --customer-confirm PROB     probability to generate a confirm operation");
        out.println("  --customer-query PROB       probability to generate a query operation");
        out.println("  --config FILENAME           configuration filename to load defaults");
        out.println("  --phase PHASE               phase name for the experiment, ex: Phase1");
        out.println("  --help                      shows this message and exits");
    }

    /* ******************************************************
     * *
     * * READ CONFIGURATION FROM FILE
     * *
     * ******************************************************/
    
    private static Properties readConfig(Properties defaults, Properties options) {
        String configFilename = options.getProperty("config", defaults.getProperty("config"));
        
        Properties current = new Properties(defaults);
        try (Reader reader = new FileReader(configFilename)) {
            current.load(reader);
        } catch (FileNotFoundException ex) {
            System.err.println("Warning: file not found: '"+configFilename+"'");
        } catch (IOException ex) {
            System.err.println("Warning: error reading file: '"+configFilename+"'");
        } 
        
        current.putAll(options);
        return current;
    }

    /* ******************************************************
     * *
     * * VALUES CHECKER
     * *
     * ******************************************************/
    
    private InetSocketAddress addressFromConfig(String key) {
        String value = config.getProperty(key);
        if (value == null) throw new IllegalStateException("Illegal configuration: "+key+" must be defined");
        if (!value.contains(":")) throw new IllegalStateException("Illegal configuration: "+key+" should contain a machine':'port");
        
        String hostname = value.split(":")[0];
        if (hostname.length() == 0) throw new IllegalStateException("Illegal configuration: "+key+" machine should contain a should not be empty");
        
        int port;
        try {
            port = Integer.parseInt(value.split(":")[1]);
            if (port < 1024) throw new IllegalStateException("Illegal configuration: "+key+" port should be at least 1024");
            if (port > 1024*64) throw new IllegalStateException("Illegal configuration: "+key+" should be less than 64K");
        } catch (NumberFormatException nfe) {
            throw new IllegalStateException("Illegal configuration: "+key+" port should be a number", nfe);
        }

        return new InetSocketAddress(hostname, port);
    }

    private boolean booleanFromConfig(String key) {
        String value = config.getProperty(key);
        if (value.equals("yes")) return true;
        if (value.equals("no")) return false;
        throw new IllegalStateException("Illegal configuration: "+key+" should be a 'yes' or 'no', it was: '"+value+"'");
    }
    
    private int numberFromConfig(String key) {
        String value = config.getProperty(key);
        int number;
        try {
            number = Integer.parseInt(value);            
        } catch (NumberFormatException nfe) {
            throw new IllegalStateException("Illegal configuration: "+key+" should be a number, it was: '"+value+"'", nfe);
        }
        return number;
    }
    
    private double floatFromConfig(String key) {
        String value = config.getProperty(key);
        double coma;
        try {
            coma = Double.parseDouble(value);            
        } catch (NumberFormatException nfe) {
            throw new IllegalStateException("Illegal configuration: "+key+" should be a float, it was: '"+value+"'", nfe);
        }
        return coma;
    }

	public void dump() {
		try {
			config.store(System.out, "# Configuration of the current node");
		} catch (IOException e) {
		}
	}
    
}
